| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- import { notFound } from 'next/navigation';
- import Image from 'next/image';
- import { fetchJson } from '@/lib/utils/server';
- import type { GameDetailResponse } from '@/types/response/store/game';
- import GameMediaGallery from './GameMediaGallery';
- import GameProductCarousel from './GameProductCarousel';
- export async function generateMetadata({ params }: { params: Promise<{ id: string }> })
- {
- const { id } = await params;
- const res = await fetchJson<GameDetailResponse>(`/api/store/games/${id}`, { method: 'GET' });
- return { title: res.success && res.data ? res.data.korName : '게임' };
- }
- export default async function GameDetailPage({ params }: { params: Promise<{ id: string }> })
- {
- const { id } = await params;
- const res = await fetchJson<GameDetailResponse>(`/api/store/games/${id}`, { method: 'GET' });
- if (!res.success || !res.data) {
- notFound();
- }
- const game = res.data;
- return (
- <div className="mx-auto w-full max-w-screen-xl px-4 py-6">
- <header className="mb-5">
- <h1 className="text-2xl font-bold">{game.korName}</h1>
- {game.engName && <p className="text-sm text-[var(--text-secondary)]">{game.engName}</p>}
- </header>
- <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
- {/* 좌측: 미디어 갤러리 + 상품 + 설명 */}
- <div className="flex min-w-0 flex-col gap-6 lg:col-span-2">
- <GameMediaGallery key={game.id} trailerUrl={game.trailerUrl} images={game.images} />
- <GameProductCarousel products={game.randomProducts} />
- {game.description && (
- <section>
- <h2 className="mb-3 text-lg font-bold">게임 소개</h2>
- <div className="max-w-none text-sm leading-relaxed [&_a]:text-[var(--text-link)] [&_a]:underline [&_h1]:mb-1 [&_h1]:mt-3 [&_h1]:text-lg [&_h1]:font-bold [&_h2]:mb-1 [&_h2]:mt-3 [&_h2]:font-bold [&_img]:my-2 [&_img]:rounded [&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_p]:mb-2 [&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5" dangerouslySetInnerHTML={{ __html: game.description }} />
- </section>
- )}
- </div>
- {/* 우측: 정보 사이드바 */}
- <aside className="flex flex-col gap-4">
- {(game.bigImage || game.thumbnail) && (
- <div className="relative aspect-video w-full overflow-hidden rounded-xl bg-[var(--bg-elevated)]">
- <Image src={(game.bigImage ?? game.thumbnail)!} alt={game.korName} fill unoptimized sizes="(max-width: 1024px) 100vw, 33vw" className="object-cover" />
- </div>
- )}
- <dl className="flex flex-col gap-3 rounded-xl border border-[var(--border-default)] bg-[var(--bg-elevated)] p-4 text-sm">
- <div className="flex justify-between gap-3">
- <dt className="text-[var(--text-secondary)]">퍼블리셔</dt>
- <dd className="text-right font-medium">{game.publisher}</dd>
- </div>
- {game.releaseDate && (
- <div className="flex justify-between gap-3">
- <dt className="text-[var(--text-secondary)]">출시일</dt>
- <dd className="text-right font-medium">{game.releaseDate.split('-').join('.')}</dd>
- </div>
- )}
- {game.genres.length > 0 && (
- <div className="flex flex-col gap-1">
- <dt className="text-[var(--text-secondary)]">장르</dt>
- <dd className="flex flex-wrap gap-1.5">
- {game.genres.map(g => (
- <span key={g} className="rounded-full border border-[var(--border-default)] px-2 py-0.5 text-xs">{g}</span>
- ))}
- </dd>
- </div>
- )}
- {game.categories.length > 0 && (
- <div className="flex flex-col gap-1">
- <dt className="text-[var(--text-secondary)]">카테고리</dt>
- <dd className="flex flex-wrap gap-1.5">
- {game.categories.map(c => (
- <span key={c} className="rounded-full border border-[var(--border-default)] px-2 py-0.5 text-xs">{c}</span>
- ))}
- </dd>
- </div>
- )}
- </dl>
- {game.languages.length > 0 && (
- <div className="rounded-xl border border-[var(--border-default)] bg-[var(--bg-elevated)] p-4">
- <h2 className="mb-2 text-sm font-bold">지원 언어</h2>
- <table className="w-full text-sm">
- <thead>
- <tr className="text-xs text-[var(--text-secondary)]">
- <th className="py-1 text-left font-medium">언어</th>
- <th className="py-1 text-center font-medium">인터페이스</th>
- <th className="py-1 text-center font-medium">음성</th>
- <th className="py-1 text-center font-medium">자막</th>
- </tr>
- </thead>
- <tbody>
- {game.languages.map(l => (
- <tr key={l.korName} className="border-t border-[var(--border-default)]">
- <td className="py-1.5">{l.korName}</td>
- <td className="py-1.5 text-center text-[var(--text-link)]">{l.interface ? '✓' : ''}</td>
- <td className="py-1.5 text-center text-[var(--text-link)]">{l.fullAudio ? '✓' : ''}</td>
- <td className="py-1.5 text-center text-[var(--text-link)]">{l.subtitles ? '✓' : ''}</td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- )}
- {(game.minSpec || game.recommendedSpec) && (
- <div className="flex flex-col gap-3 rounded-xl border border-[var(--border-default)] bg-[var(--bg-elevated)] p-4 text-sm">
- <h2 className="font-bold">시스템 사양</h2>
- {game.minSpec && (
- <div>
- <p className="mb-1 text-[var(--text-secondary)]">최소 사양</p>
- <p className="whitespace-pre-line">{game.minSpec}</p>
- </div>
- )}
- {game.recommendedSpec && (
- <div>
- <p className="mb-1 text-[var(--text-secondary)]">권장 사양</p>
- <p className="whitespace-pre-line">{game.recommendedSpec}</p>
- </div>
- )}
- </div>
- )}
- {game.link && (
- <a
- href={game.link}
- target="_blank"
- rel="noopener noreferrer"
- className="rounded-lg bg-[var(--text-link)] px-4 py-2.5 text-center text-sm font-semibold text-white"
- >
- 공식 사이트 방문
- </a>
- )}
- </aside>
- </div>
- </div>
- );
- }
|